<!--
 * @Author: Penk
 * @LastEditors: Penk
 * @LastEditTime: 2023-10-13 10:03:55
 * @FilePath: \web-interview-questions\组件\制作视频封面.html
 * @email: 492934056@qq.com
-->
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <!-- Import style -->
    <link rel="stylesheet" href="https://unpkg.com/element-plus/dist/index.css" />
    <!-- Import Vue 3 -->
    <script src="https://unpkg.com/vue@3"></script>
    <!-- Import component library -->
    <script src="https://unpkg.com/element-plus"></script>
    <title>Document</title>
    <style>
      * {
        margin: 0;
        padding: 0;
      }

      #app {
        display: flex;
        width: 100%;
        height: 100vh;
        overflow: hidden;
      }

      #app aside {
        min-width: 400px;
        background-color: antiquewhite;
        display: flex;
        flex-direction: column;
        padding: 20px;
        position: relative;
      }

      #app aside .title {
        margin-bottom: 20px;
      }

      #app main {
        background-color: #fff;
        flex: 1;
        padding: 20px;
      }

      .downloadBtn_box {
        position: absolute;
        bottom: 20px;
        right: 20px;
      }
    </style>
  </head>

  <body>
    <div id="app">
      <aside>
        <h1 class="title">视频封面制作</h1>
        <el-tabs v-model="activeName" class="demo-tabs">
          <el-tab-pane label="标题" name="first">
            <el-row style="margin-top: 20px">
              <el-form label-width="120px">
                <div v-for="item in config.inputs" style="margin-bottom: 30px">
                  <h3>{{item.title}}</h3>
                  <el-form-item label="文本">
                    <el-input v-model.lazy="item.value" />
                  </el-form-item>
                  <el-form-item label="字体大小">
                    <el-input v-model.lazy.number="item.fontSize" />
                  </el-form-item>
                </div>
              </el-form>
            </el-row>
          </el-tab-pane>
          <el-tab-pane label="视频封面" name="second">
            <h2>视频封面</h2>
            <el-row style="margin-top: 20px">
              <el-input v-model.lazy.number="config.width" placeholder="Please input">
                <template #prepend>视频封面宽度</template>
              </el-input>
            </el-row>
            <el-row style="margin-top: 20px">
              <el-input v-model.lazy.number="config.height" placeholder="Please input">
                <template #prepend>视频封面高度</template>
              </el-input>
            </el-row>
            <el-row style="margin-top: 20px">
              <div>
                <span>开始渐变色：</span>
                <!-- 'hsl' | 'hsv' | 'hex' | 'rgb' | 'hex' (when show-alpha is false)
                   | 'rgb' (when show-alpha is true) -->
                <el-color-picker color-format="hex" v-model.lazy="config.beginColor" show-alpha />
              </div>
            </el-row>
            <el-row style="margin-top: 20px">
              <div>
                <span>结束渐变色：</span>
                <el-color-picker color-format="hex" v-model.lazy="config.endColor" show-alpha />
              </div>
            </el-row>
            <el-row style="margin-top: 20px">
              <div>
                <span>文字颜色：</span>
                <el-color-picker
                  color-format="hex"
                  v-model.lazy="config.mainTextColor"
                  show-alpha
                />
              </div>
            </el-row>
            <el-row style="margin-top: 20px">
              <div>
                <span>主文字阴影颜色：</span>
                <el-color-picker
                  color-format="hex"
                  v-model.lazy="config.mainTextShadowColor"
                  show-alpha
                />
              </div>
            </el-row>
          </el-tab-pane>
        </el-tabs>
        <div class="downloadBtn_box">
          <el-button type="primary" @click="clearCache">清空本地缓存</el-button>
          <el-button type="primary" @click="downloadPic">下载图片</el-button>
        </div>
      </aside>
      <main>
        <canvas id="myCanvas"></canvas>
      </main>
    </div>

    <script>
      const { createApp } = Vue;

      // 防抖
      function debounce(fn, timeout = 10) {
        let timer = null;
        return function (n, o) {
          // 如果已经有事件了，则清空计时器重新执行新的，保证是最新一次执行
          if (timer != null) {
            clearTimeout(timer);
          }
          // 这里需要箭头函数，这样就可以提升到input2这个作用域
          timer = setTimeout(() => {
            fn.call(this, n, o);
          }, timeout);
        };
      }

      let app = {
        data() {
          return {
            config: {
              inputs: [
                {
                  id: 1,
                  title: '标题',
                  value: '# Web前端面试题',
                  x: 50,
                  y: 100,
                  z: 0,
                  fontSize: 80,
                  type: 'type1',
                  isSelect: false
                },
                {
                  id: 2,
                  title: '副标题1',
                  value: '副标题第一行222',
                  x: 50,
                  y: 230,
                  z: 1,
                  fontSize: 60,
                  type: 'type2',
                  isSelect: false
                },
                {
                  id: 3,
                  title: '副标题2',
                  value: '副标题第二行333',
                  x: 50,
                  y: 300,
                  z: 2,
                  fontSize: 60,
                  type: 'type2',
                  isSelect: false
                }
              ],
              width: 800,
              height: 400,
              beginColor: '#3C3B3BFF',
              endColor: '#000000FF',
              mainTextColor: '#fff',
              mainTextShadowColor: '#63631FFF'
            },
            ctx: null,
            cvs: null,
            isMounted: false,
            // activeName: 'first',
            activeName: 'second',
            // 是否拖拽中
            isDragging: false,
            // 储存对象
            draggingEle: null,
            maxZ: -1
          };
        },
        watch: {
          // 配置变动，则自动修改canvas，以及本地缓存
          config: {
            handler: debounce(function (n, o) {
              console.log('watch...');
              if (!this.isMounted) return;

              localStorage.setItem('config', JSON.stringify(n));
              requestAnimationFrame(this.render);
            }, 0),
            deep: true
          }
        },
        created() {},
        mounted() {
          this.isMounted = true;
          // 初始化画布
          this.cvs = document.getElementById('myCanvas');
          if (this.cvs && this.cvs.getContext) {
            this.ctx = this.cvs.getContext('2d');
            this.addEventListener();
          }

          // this.render(true);
          // 获取本地缓存信息
          let config = JSON.parse(localStorage.getItem('config'));
          if (config) {
            console.log('localStorage有数据，触发watch...');
            this.config = config;
          } else {
            console.log('localStorage没有数据，触发render...');
            this.render(true);
          }
        },
        destroyed() {
          this.removeEventListener();
        },
        methods: {
          // 清空本地缓存并且刷新
          clearCache() {
            localStorage.removeItem('config');
            history.go(0);
          },
          // 下载图片
          downloadPic() {
            let ctx = document.getElementById('myCanvas');
            // 创建一个 a 标签，并设置 href 和 download 属性
            const el = document.createElement('a');
            // 设置 href 为图片经过 base64 编码后的字符串，默认为 png 格式
            el.href = ctx.toDataURL();
            el.download = '视频封面' + new Date().toTimeString() + '.png';

            // 创建一个点击事件并对 a 标签进行触发
            const event = new MouseEvent('click');
            el.dispatchEvent(event);
          },
          // 初始化画布
          // 设置背景
          initCanvas(isForce) {
            // 清空背景
            this.ctx.clearRect(0, 0, this.config.width, this.config.height);

            // console.log(
            //   this.config.width,
            //   this.cvs.width,
            //   this.config.height,
            //   this.cvs.height
            // );
            if (
              this.config.width == this.cvs.width &&
              this.config.height == this.cvs.height &&
              !isForce
            ) {
              // console.log("宽高不变，不需要重新渲染背景...");
              return;
            } else {
              // console.log("重新渲染背景...");
            }

            this.cvs.width = parseInt(this.config.width);
            this.cvs.height = parseInt(this.config.height);
            // 设置背景色
            var grd = this.ctx.createLinearGradient(0, 0, 0, this.config.height);

            grd.addColorStop(0, this.config.beginColor);
            grd.addColorStop(1, this.config.endColor);
            this.ctx.fillStyle = grd;
            this.ctx.fillRect(0, 0, this.config.width, this.config.height);
          },
          // 写文字
          writeText(text, x, y, size) {
            if (!text) return;

            // //水平渐变
            // var lineGrad = this.ctx.createLinearGradient(0, 0, 200, 400);
            // //加上颜色
            // lineGrad.addColorStop(0.5, '#eee');
            // lineGrad.addColorStop(1, '#fff');
            // 加阴影
            this.ctx.shadowColor = this.config.mainTextShadowColor;
            this.ctx.shadowBlur = 10; //模糊度
            this.ctx.shadowOffsetX = 10; //水平方向偏移量，大于零往左，
            this.ctx.shadowOffsetY = 5; //垂直方向偏移量，大于零往下
            this.ctx.font = `bold    ${size}  auto`;
            this.ctx.textBaseline = 'top';
            // font-weight font-style(正常 倾斜 斜线) font-size font-family
            // this.ctx.fillStyle = lineGrad;
            this.ctx.fillStyle = this.config.mainTextColor;
            this.ctx.fillText(text, x, y);

            return {
              text,
              x,
              y: y,
              width: this.ctx.measureText(text).width,
              height: parseInt(size)
            };
          },
          writeText2(text, x, y, size) {
            if (!text) return;

            this.ctx.shadowColor = null;
            this.ctx.shadowBlur = null; //模糊度
            this.ctx.shadowOffsetX = null; //水平方向偏移量，大于零往左，
            this.ctx.shadowOffsetY = null; //垂直方向偏移量，大于零往下
            this.ctx.font = `${size}  cursive`;
            this.ctx.textBaseline = 'top';
            this.ctx.fillStyle = this.config.mainTextColor;
            this.ctx.fillText(text, x, y);
            return {
              text,
              x,
              y: y,
              width: this.ctx.measureText(text).width,
              height: parseInt(size)
            };
          },
          // 渲染
          render(isForce = false) {
            console.log('render...');
            // 怕width变动，所以重新渲染背景图，这里也可以优化~~~
            this.initCanvas(isForce);

            // 保存各个元素的信息
            var ele = null;
            var fn = null;

            // 弄多个临时变量，防止watch...
            // 这个用来排序的
            let inputs = [...this.config.inputs];
            // 需要排序，层级问题
            inputs.sort((a, b) => {
              return a.z - b.z;
            });
            // // 保存最高Z
            // if (inputs.length > 0) {
            //   this.maxZ = inputs[inputs.length - 1].z;
            // }

            inputs.forEach((item, index, arr) => {
              // 如果是被选择，那就单独提取出去，绘图放在mousemove，减少watch...
              if (item.isSelect) return;

              if (item.z > this.maxZ) {
                this.maxZ = item.z;
              }

              switch (item.type) {
                case 'type1': {
                  fn = this.writeText;
                  break;
                }
                case 'type2': {
                  fn = this.writeText2;
                  break;
                }
                default:
                  break;
              }
              ele = fn(item.value, item.x, item.y, item.fontSize + 'px');
              if (ele) {
                arr[index] = item = {
                  ...item,
                  ...ele
                };
              }
            });

            console.log(inputs);

            // 重新排序，避免配置栏混乱
            inputs.sort((a, b) => {
              return a.id - b.id;
            });

            // inputs有变动才赋值，避免watch无限调用...
            if (JSON.stringify(this.config.inputs) != JSON.stringify(inputs)) {
              this.config.inputs = inputs;
              console.log(
                '初始化元素的位置信息，或者mouseup触发watch...，要画到canvas才会有一些位置信息~'
              );
            } else {
              // console.log("input不用重新赋值...");
            }

            if (this.draggingEle) {
              // 绘制移动中的文字
              switch (this.draggingEle.type) {
                case 'type1': {
                  fn = this.writeText;
                  break;
                }
                case 'type2': {
                  fn = this.writeText2;
                  break;
                }
                default:
                  break;
              }
              fn(
                this.draggingEle.value,
                this.draggingEle.x,
                this.draggingEle.y,
                this.draggingEle.fontSize + 'px'
              );

              // 绘制方框
              let offset = 7;
              let { x, y, width, height } = this.draggingEle;
              x = x - offset;
              y = y - offset;
              width = width + offset * 2;
              height = height + offset * 2;

              this.ctx.beginPath();
              this.ctx.moveTo(x, y);
              this.ctx.lineTo(x + width, y);
              this.ctx.lineTo(x + width, y + height);
              this.ctx.lineTo(x, y + height);
              this.ctx.lineTo(x, y);
              this.ctx.stroke();
            }
          },
          // mousedown事件
          mousedown(e) {
            var x = e.offsetX,
              y = e.offsetY;
            // console.log("mousedown:", x, y);

            // 弄多个临时变量，防止watch...
            let idx = -1;
            let inputs = [...this.config.inputs];
            // 保存索引
            inputs.forEach((e, index, arr) => {
              arr[index].tempIdx = index;
            });

            // 过滤选中的
            inputs = inputs.filter((element) => {
              return (
                x > element.x &&
                x < element.x + element.width &&
                y > element.y &&
                y < element.y + element.height
              );
            });

            // 排序
            inputs.sort((a, b) => {
              return a.z - b.z;
            });

            // 有的话就中
            if (inputs.length > 0) {
              this.isDragging = true;
              this.config.inputs[inputs[inputs.length - 1].tempIdx].isSelect = true;
              this.draggingEle = { ...inputs[inputs.length - 1] };
              this.draggingEle.x1 = e.offsetX;
              this.draggingEle.y1 = e.offsetY;

              console.log('拖拽：', this.draggingEle);
            }
          },
          // mousemove事件
          mousemove(e) {
            console.log('mousemove...');
            if (this.isDragging) {
              console.log('mousemove...在拖拽，将会触发render...');
              // 获取当前鼠标坐标
              let x2 = e.offsetX;
              let y2 = e.offsetY;

              // 减去之前的坐标，得到2次鼠标间偏移量
              let offsetX = x2 - this.draggingEle.x1;
              let offsetY = y2 - this.draggingEle.y1;

              // 重新计算得出新坐标
              this.draggingEle.x = this.draggingEle.x + offsetX;
              this.draggingEle.y = this.draggingEle.y + offsetY;

              // 保存当前鼠标坐标
              this.draggingEle.x1 = e.offsetX;
              this.draggingEle.y1 = e.offsetY;

              requestAnimationFrame(this.render);
            }
          },
          // mouseup事件
          mouseup(e) {
            this.isDragging = false;
            console.log('mouseup');
            if (!this.draggingEle) return;

            let index = this.draggingEle.tempIdx;
            this.config.inputs[index] = {
              ...this.config.inputs[index],
              x: this.draggingEle.x,
              y: this.draggingEle.y,
              z: this.maxZ + 1,
              isSelect: false
            };
            this.draggingEle = null;
          },

          // 添加绑定事件
          addEventListener() {
            this.cvs.addEventListener('mousedown', this.mousedown);
            this.cvs.addEventListener('mousemove', this.mousemove);
            document.addEventListener('mouseup', this.mouseup);
          },
          removeEventListener() {
            this.cvs.removeEventListener('mousedown', this.mousedown);
            this.cvs.removeEventListener('mousemove', this.mousemove);
            document.removeEventListener('mouseup', this.mouseup);
          }
        }
      };

      // 配置vue以及element-ui
      createApp(app).use(ElementPlus).mount('#app');
    </script>
  </body>
</html>
